package com.kotasoft.training.roogwteclipselink.client.scaffold.place;

import com.google.gwt.activity.shared.Activity;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.place.shared.Place;
import com.google.gwt.place.shared.PlaceChangeEvent;
import com.google.gwt.place.shared.PlaceController;
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.EntityProxyChange;
import com.google.gwt.requestfactory.shared.EntityProxyId;
import com.google.gwt.requestfactory.shared.Receiver;
import com.google.gwt.requestfactory.shared.Request;
import com.google.gwt.requestfactory.shared.WriteOperation;
import com.google.gwt.user.cellview.client.AbstractHasData;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.Range;
import com.google.gwt.view.client.RangeChangeEvent;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SingleSelectionModel;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Abstract activity for displaying a list of {@link EntityProxy}. These
 * activities are not re-usable. Once they are stopped, they cannot be
 * restarted.
 * <p>
 * Subclasses must:
 * 
 * <ul>
 * <li>provide a {@link ProxyListView}
 * <li>implement method to request a full count
 * <li>implement method to find a range of entities
 * <li>respond to "show details" commands
 * </ul>
 * <p>
 * Only the properties required by the view will be requested.
 * 
 * @param <P> the type of {@link EntityProxy} listed
 */
public abstract class AbstractProxyListActivity<P extends EntityProxy>
    implements Activity, ProxyListView.Delegate<P> {

  /**
   * This mapping allows us to update individual rows as records change.
   */
  private final Map<EntityProxyId<P>, Integer> idToRow = new HashMap<EntityProxyId<P>, Integer>();
  private final Map<EntityProxyId<P>, P> idToProxy = new HashMap<EntityProxyId<P>, P>();

  private final PlaceController placeController;
  private final SingleSelectionModel<P> selectionModel;
  private final Class<P> proxyClass;

  private HandlerRegistration rangeChangeHandler;
  private ProxyListView<P> view;
  private AcceptsOneWidget display;

  public AbstractProxyListActivity(PlaceController placeController,
      ProxyListView<P> view, Class<P> proxyType) {
    this.view = view;
    this.placeController = placeController;
    this.proxyClass = proxyType;
    view.setDelegate(this);

    final HasData<P> hasData = view.asHasData();
    rangeChangeHandler = hasData.addRangeChangeHandler(new RangeChangeEvent.Handler() {
      public void onRangeChange(RangeChangeEvent event) {
        AbstractProxyListActivity.this.onRangeChanged(hasData);
      }
    });

    // Inherit the view's key provider
    ProvidesKey<P> keyProvider = ((AbstractHasData<P>) hasData).getKeyProvider();
    selectionModel = new SingleSelectionModel<P>(keyProvider);
    hasData.setSelectionModel(selectionModel);

    selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
      public void onSelectionChange(SelectionChangeEvent event) {
        P selectedObject = selectionModel.getSelectedObject();
        if (selectedObject != null) {
          showDetails(selectedObject);
        }
      }
    });
  }

  public void createClicked() {
    placeController.goTo(new ProxyPlace(proxyClass));
  }

  public ProxyListView<P> getView() {
    return view;
  }

  public String mayStop() {
    return null;
  }

  public void onCancel() {
    onStop();
  }

  /**
   * Called by the table as it needs data.
   */
  public void onRangeChanged(HasData<P> listView) {
    final Range range = listView.getVisibleRange();

    final Receiver<List<P>> callback = new Receiver<List<P>>() {
      @Override
      public void onSuccess(List<P> values) {
        if (view == null) {
          // This activity is dead
          return;
        }
        idToRow.clear();
        idToProxy.clear();
        for (int i = 0, row = range.getStart(); i < values.size(); i++, row++) {
          P proxy = values.get(i);
          @SuppressWarnings("unchecked")
          // Why is this cast needed?
          EntityProxyId<P> proxyId = (EntityProxyId<P>) proxy.stableId();
          idToRow.put(proxyId, row);
          idToProxy.put(proxyId, proxy);
        }
        getView().asHasData().setRowData(range.getStart(), values);
        if (display != null) {
          display.setWidget(getView());
        }
      }
    };

    fireRangeRequest(range, callback);
  }

  public void onStop() {
    view.setDelegate(null);
    view = null;
    rangeChangeHandler.removeHandler();
    rangeChangeHandler = null;
  }

  /**
   * Select the given record, or clear the selection if called with null or an
   * id we don't know.
   */
  public void select(EntityProxyId<P> proxyId) {
    /*
     * The selectionModel will not flash if we put it back to the same state it
     * is already in, so we can keep this code simple.
     */

    // Clear the selection
    P selected = selectionModel.getSelectedObject();
    if (selected != null) {
      selectionModel.setSelected(selected, false);
    }

    // Select the new proxy, if it's relevant
    if (proxyId != null) {
      P selectMe = idToProxy.get(proxyId);
      selectionModel.setSelected(selectMe, true);
    }
  }

  public void start(AcceptsOneWidget display, EventBus eventBus) {
    view.setDelegate(this);
    EntityProxyChange.registerForProxyType(eventBus, proxyClass,
        new EntityProxyChange.Handler<P>() {
          public void onProxyChange(EntityProxyChange<P> event) {
            update(event.getWriteOperation(), event.getProxyId());
          }
        });
    eventBus.addHandler(PlaceChangeEvent.TYPE, new PlaceChangeEvent.Handler() {
      public void onPlaceChange(PlaceChangeEvent event) {
        updateSelection(event.getNewPlace());
      }
    });
    this.display = display;
    init();
    updateSelection(placeController.getWhere());
  }

  public void update(WriteOperation writeOperation, EntityProxyId<P> proxyId) {
    switch (writeOperation) {
      case UPDATE:
        update(proxyId);
        break;

      case DELETE:
        init();
        break;

      case PERSIST:
        /*
         * On create, we presume the new record is at the end of the list, so
         * fetch the last page of items.
         */
        getLastPage();
        break;
    }
  }

  protected abstract Request<List<P>> createRangeRequest(Range range);

  protected abstract void fireCountRequest(Receiver<Long> callback);

  /**
   * Called when the user chooses a record to view. This default implementation
   * sends the {@link PlaceController} to an appropriate {@link ProxyPlace}.
   * 
   * @param record the chosen record
   */
  protected void showDetails(P record) {
    placeController.goTo(new ProxyPlace(record.stableId(), ProxyPlace.Operation.DETAILS));
  }

  @SuppressWarnings("unchecked")
  private EntityProxyId<P> cast(ProxyPlace proxyPlace) {
    return (EntityProxyId<P>) proxyPlace.getProxyId();
  }

  private void fireRangeRequest(final Range range,
      final Receiver<List<P>> callback) {
    createRangeRequest(range).with(getView().getPaths()).fire(callback);
  }

  private void getLastPage() {
    fireCountRequest(new Receiver<Long>() {
      @Override
      public void onSuccess(Long response) {
        if (view == null) {
          // This activity is dead
          return;
        }
        HasData<P> table = getView().asHasData();
        int rows = response.intValue();
        table.setRowCount(rows, true);
        if (rows > 0) {
          int pageSize = table.getVisibleRange().getLength();
          int remnant = rows % pageSize;
          if (remnant == 0) {
            table.setVisibleRange(rows - pageSize, pageSize);
          } else {
            table.setVisibleRange(rows - remnant, pageSize);
          }
        }
        onRangeChanged(table);
      }
    });
  }

  private void init() {
    fireCountRequest(new Receiver<Long>() {
      @Override
      public void onSuccess(Long response) {
        if (view == null) {
          // This activity is dead
          return;
        }
        getView().asHasData().setRowCount(response.intValue(), true);
        onRangeChanged(view.asHasData());
      }
    });
  }

  private void update(EntityProxyId<P> proxyId) {
    final Integer row = idToRow.get(proxyId);
    if (row == null) {
      return;
    }
    fireRangeRequest(new Range(row, 1), new Receiver<List<P>>() {
      @Override
      public void onSuccess(List<P> response) {
        getView().asHasData().setRowData(row,
            Collections.singletonList(response.get(0)));
      }
    });
  }

  private void updateSelection(Place newPlace) {
    if (newPlace instanceof ProxyPlace) {
      ProxyPlace proxyPlace = (ProxyPlace) newPlace;
      if (proxyPlace.getOperation() != ProxyPlace.Operation.CREATE
          && proxyPlace.getProxyClass().equals(proxyClass)) {
        select(cast(proxyPlace));
        return;
      }
    }

    select(null);
  }
}
